SPB Git
13.8 KB · 128 lines tsx
1import type { Metadata } from "next";2import Link from "next/link";3import { notFound, permanentRedirect } from "next/navigation";4import { BookOpen, Download, History } from "lucide-react";5import { getLocale } from "@/lib/i18n/server";6import { fmtDate, fmtInt, href, t, type Locale } from "@/lib/i18n";7import { findRedirect, getPaper } from "@/lib/papers/queries";8import { parseIdentifier } from "@/lib/papers/identifiers";9import { citationApa, citationChicago } from "@/lib/papers/citation";10import { citable, jsonLd, scholarMeta } from "@/lib/papers/meta";11import type { PaperFull } from "@/lib/papers/types";12import { AuthorsLine, StatusChips } from "@/components/paper-items";13import { CiteMenu } from "@/components/cite-menu";14import { ViewBeacon } from "@/components/view-beacon";15import { APP_URL } from "@/lib/env";16export const revalidate = 120;1718async function load(id: string) {19  const parsed = parseIdentifier(id); const p = await getPaper(id);20  if (!p) { const r = await findRedirect(`/papers/${id}`); if (r) permanentRedirect(r.to_path); notFound(); }21  if (parsed && parsed.slug !== id) permanentRedirect(`/papers/${parsed.slug}`);22  return p;23}24export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {25  const { id } = await params; const p = await getPaper(id); if (!p) return {};26  const other = scholarMeta(p);27  return { title: `${p.identifier} · ${p.title}`, description: p.abstract[0]?.slice(0, 300), alternates: { canonical: `/papers/${p.slug}`, languages: { en: `/papers/${p.slug}`, fr: `/fr/papers/${p.slug}` } }, openGraph: { type: "article", title: p.title, description: p.abstract[0]?.slice(0, 300), url: `${APP_URL}/papers/${p.slug}`, images: [`/api/preview/${p.slug}.png`] }, other };28}2930function Section({ id, title, children }: { id: string; title: string; children: React.ReactNode }) { return <section id={id} className="mt-10"><h2 className="text-xl sm:text-2xl mb-3">{title}</h2>{children}</section>; }31function Pairs({ items, locale }: { items: { title: string; description: string; kind?: string }[]; locale: Locale }) {32  return <dl className="grid gap-3 sm:grid-cols-2">{items.map((c, i) => <div key={i} className="card p-4"><dt className="font-medium text-fg">{c.kind ? <span className="label block mb-1">{c.kind}</span> : null}{c.title}</dt><dd className="text-sm text-fg-2 mt-1 leading-relaxed">{c.description}</dd></div>)}</dl>;33}34const lname = (x: { name_en: string; name_fr: string }, locale: Locale) => (locale === "fr" ? x.name_fr : x.name_en);3536export default async function PaperPage({ params }: { params: Promise<{ id: string }> }) {37  const { id } = await params; const locale = await getLocale(); const d = t(locale); const p: PaperFull = await load(id);38  const c = citable(p, locale); const apa = citationApa(c), chicago = citationChicago(c);39  const pdfUrl = p.has_pdf && p.file_id ? `/api/files/${p.file_id}/${p.slug}-${p.version_label ?? "current"}.pdf` : null;40  const abstract = locale === "fr" && p.abstract_fr?.length ? p.abstract_fr : p.abstract; const frOnly = locale === "fr" && !p.abstract_fr?.length;41  const ld = jsonLd(p);42  return (43    <article className="container py-10">44      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }} />45      <ViewBeacon slug={p.slug} />46      {p.status === "withdrawn" && <p className="card p-4 mb-6 border-burgundy text-burgundy">{d.paper.withdrawnNotice}</p>}47      {p.status === "superseded" && <p className="card p-4 mb-6">{d.paper.supersededNotice}</p>}48      <header>49        <p className="kicker">{locale === "fr" ? p.series_name_fr : p.series_name_en} · <span className="font-mono">{p.identifier}</span>{p.version_label ? ` · ${p.version_label}` : ""}</p>50        <h1 className="mt-3 text-3xl sm:text-4xl leading-tight text-fg">{locale === "fr" && p.title_fr ? p.title_fr : p.title}</h1>51        {p.subtitle && <p className="mt-2 text-lg text-fg-2 serif">{p.subtitle}</p>}52        <p className="mt-4 text-fg-2"><AuthorsLine authors={p.authors} locale={locale} /></p>53        <dl className="mt-5 grid gap-x-6 gap-y-2 grid-cols-2 sm:grid-cols-4 text-sm">54          <div><dt className="label">{d.paper.year}</dt><dd>{p.year ?? "—"}</dd></div>55          <div><dt className="label">{d.paper.released}</dt><dd>{fmtDate(locale, p.first_released_on, p.first_released_precision) || "—"}</dd></div>56          <div><dt className="label">{d.paper.revised}</dt><dd>{fmtDate(locale, p.last_revised_on, p.last_revised_precision) || "—"}</dd></div>57          <div><dt className="label">{d.paper.pubStatus}</dt><dd>{d.misc.pubStatus[p.publication_status as keyof typeof d.misc.pubStatus]}{p.journal_ref ? ` — ${p.journal_ref}` : ""}</dd></div>58        </dl>59        <div className="mt-3"><StatusChips p={p} locale={locale} /></div>60        <div className="mt-6 flex flex-wrap gap-2">61          {pdfUrl ? <a href={pdfUrl} className="btn btn-primary no-underline"><Download size={16} aria-hidden /> {d.paper.download}{p.pages ? ` (${p.pages} ${d.papers.pages})` : ""}</a> : <span className="btn opacity-60 cursor-not-allowed">{d.paper.notAvailable}</span>}62          {pdfUrl && <Link href={href(locale, `/papers/${p.slug}/read`)} className="btn no-underline"><BookOpen size={16} aria-hidden /> {d.paper.read}</Link>}63          <CiteMenu locale={locale} slug={p.slug} apa={apa} chicago={chicago} shareUrl={`${APP_URL}/papers/${p.slug}`} />64          {p.versions.length > 1 && <a href="#versions" className="btn no-underline"><History size={16} aria-hidden /> {d.paper.previousVersions}</a>}65        </div>66      </header>6768      <div className="grid gap-10 lg:grid-cols-[1fr_19rem] mt-10">69        <div className="min-w-0">70          {p.hero_headline && <p className="serif text-xl text-fg leading-snug border-l-2 border-burgundy pl-4">{p.hero_headline}</p>}71          {p.hero_subheadline && <p className="text-fg-2 mt-2 pl-4 text-sm leading-relaxed">{p.hero_subheadline}</p>}72          <Section id="abstract" title={d.paper.abstract}>73            {frOnly && <p className="meta mb-2 italic">{d.paper.frOnly}</p>}74            <div className="prose-academic">{abstract.map((para, i) => <p key={i}>{para}</p>)}</div>75          </Section>76          {(locale === "fr" ? p.research_question_fr ?? p.research_question : p.research_question) && <Section id="question" title={d.paper.question}><p className="prose-academic">{locale === "fr" ? p.research_question_fr ?? p.research_question : p.research_question}</p></Section>}77          {p.findings.length > 0 && <Section id="findings" title={d.paper.findings}><ul className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">{p.findings.map((f, i) => <li key={i} className="card p-4"><p className="serif text-xl text-navy">{f.value}</p><p className="text-sm text-fg-2 mt-1 leading-snug">{f.label}</p></li>)}</ul></Section>}78          {p.contributions.length > 0 && <Section id="contributions" title={d.paper.contributions}><Pairs items={p.contributions} locale={locale} /></Section>}79          {p.methodology.length > 0 && <Section id="methodology" title={d.paper.methodology}><details open className="card p-4"><summary className="font-medium">{d.paper.methodology} ({p.methodology.length})</summary><div className="mt-3 grid gap-3">{p.methodology.map((m, i) => <div key={i}><p className="font-medium text-fg">{m.title}</p><p className="text-sm text-fg-2 mt-0.5 leading-relaxed">{m.description}</p></div>)}</div></details></Section>}80          {p.data_sections.length > 0 && <Section id="data" title={d.paper.data}><Pairs items={p.data_sections} locale={locale} />81            {p.datasets.length > 0 && <p className="meta mt-3">{d.paper.datasetsTag}: {p.datasets.map((x, i) => <span key={x.slug}>{i > 0 && ", "}<Link href={href(locale, `/datasets/${x.slug}`)} className="text-navy no-underline hover:underline">{lname(x, locale)}</Link>{x.assigned_by === "inferred" ? ` (${d.paper.inferred})` : ""}</span>)}</p>}82          </Section>}83          {p.reproducibility.length > 0 && <Section id="reproducibility" title={d.paper.reproducibility}><ul className="list-disc pl-5 space-y-1 text-fg-2 text-sm leading-relaxed">{p.reproducibility.map((r, i) => <li key={i}>{r}</li>)}</ul></Section>}84          <Section id="artifacts" title={d.paper.artifacts}>85            <ul className="grid gap-2 sm:grid-cols-2 text-sm">86              {pdfUrl && <li className="card px-4 py-3"><a href={pdfUrl} className="text-navy no-underline hover:underline">{d.paper.download}</a><span className="meta block">PDF · {p.version_label} · SHA-256 <code className="text-xs">{p.sha256?.slice(0, 16)}…</code></span></li>}87              {p.artifacts.map((a, i) => <li key={i} className="card px-4 py-3"><a href={a.url} rel="noopener" className="text-navy no-underline hover:underline">{a.label}</a><span className="meta block">{a.kind}</span></li>)}88              {p.ssrn_id && <li className="card px-4 py-3"><a href={`https://ssrn.com/abstract=${p.ssrn_id}`} rel="noopener" className="text-navy no-underline hover:underline">SSRN {p.ssrn_id}</a><span className="meta block">{d.paper.ssrn}</span></li>}89              {p.doi && <li className="card px-4 py-3"><a href={`https://doi.org/${p.doi}`} rel="noopener" className="text-navy no-underline hover:underline">{p.doi}</a><span className="meta block">{d.paper.doi}</span></li>}90              {p.external_links.filter((l) => l.kind !== "ssrn").map((l, i) => <li key={"x" + i} className="card px-4 py-3"><a href={l.url} rel="noopener" className="text-navy no-underline hover:underline">{l.label}</a><span className="meta block">{l.kind}</span></li>)}91            </ul>92          </Section>93          {p.related.length > 0 && <Section id="related" title={d.paper.related}>94            <ul className="grid gap-2">{p.related.map((r) => <li key={r.slug} className="card px-4 py-3"><Link href={href(locale, `/papers/${r.slug}`)} className="text-fg no-underline hover:underline"><span className="font-mono text-navy text-xs mr-2">{r.identifier}</span>{r.title}</Link><p className="meta mt-1">{d.paper.whyRelated} {(locale === "fr" ? r.explanations_fr : r.explanations).filter(Boolean).join(" · ")}{r.inferred ? ` — ${d.paper.inferred}` : ""}</p></li>)}</ul>95          </Section>}96          <Section id="versions" title={d.paper.versions}>97            <div className="overflow-x-auto card"><table className="data"><thead><tr><th>{d.paper.versionTable.version}</th><th>{d.paper.versionTable.date}</th><th>{d.paper.versionTable.description}</th><th>{d.paper.versionTable.file}</th></tr></thead>98              <tbody>{p.versions.map((v) => <tr key={v.id}><td className="whitespace-nowrap"><Link href={href(locale, `/papers/${p.slug}/v${v.version_no}`)} className="text-navy no-underline hover:underline">{v.label}</Link>{v.is_current && <span className="chip chip-navy ml-2">{d.paper.versionTable.current}</span>}{v.withdrawn && <span className="chip chip-burgundy ml-2">{d.paper.versionTable.withdrawn}</span>}</td><td className="whitespace-nowrap">{fmtDate(locale, v.released_on, v.released_precision as "day" | "month" | "year") || "—"}</td><td className="text-fg-2">{locale === "fr" ? v.description_fr ?? v.description : v.description}{v.withdrawn && v.withdrawn_reason ? ` — ${v.withdrawn_reason}` : ""}</td><td className="whitespace-nowrap">{v.file_id && !v.withdrawn ? <a href={`/api/files/${v.file_id}/${p.slug}-${v.label}.pdf`} className="text-navy no-underline hover:underline">PDF{v.pages ? ` · ${v.pages} p.` : ""}</a> : "—"}</td></tr>)}</tbody></table></div>99          </Section>100        </div>101        <aside className="space-y-6 text-sm">102          <div className="card p-4">103            <p className="label mb-2">{d.paper.keywords}</p><p className="flex flex-wrap gap-1.5">{p.keywords.map((k) => <Link key={k} href={href(locale, `/papers?keyword=${encodeURIComponent(k)}`)} className="chip no-underline">{k}</Link>)}</p>104            {p.jel.length > 0 && <><p className="label mt-4 mb-1">{d.paper.jel}</p><p className="font-mono text-fg-2">{p.jel.join(", ")}</p></>}105          </div>106          <div className="card p-4">107            <p className="label mb-2">{d.paper.topics}</p><p className="flex flex-wrap gap-1.5">{p.topics.map((x) => <Link key={x.slug} href={href(locale, `/topics/${x.slug}`)} className={`chip no-underline ${x.kind === "domain" ? "chip-navy" : ""}`}>{lname(x, locale)}{x.assigned_by === "inferred" ? "*" : ""}</Link>)}</p>108            {p.methods.length > 0 && <><p className="label mt-4 mb-2">{d.paper.methodsTag}</p><p className="flex flex-wrap gap-1.5">{p.methods.map((x) => <Link key={x.slug} href={href(locale, `/methods/${x.slug}`)} className="chip no-underline">{lname(x, locale)}</Link>)}</p></>}109            {p.topics.some((x) => x.assigned_by === "inferred") && <p className="meta mt-2">* {d.paper.inferred}</p>}110          </div>111          <div className="card p-4">112            <p className="label mb-2">{d.paper.authors}</p>113            <ul className="space-y-2">{p.authors.map((a) => <li key={a.slug}><Link href={href(locale, `/authors/${a.slug}`)} className="text-fg no-underline hover:underline">{a.display_name}</Link></li>)}</ul>114            <p className="label mt-4 mb-1">{d.paper.affiliation}</p><p className="text-fg-2">{d.site.affiliation}</p>115          </div>116          <div className="card p-4 text-fg-2">117            <p><span className="label">{d.paper.identifier}</span><br /><span className="font-mono">{p.identifier}</span></p>118            <p className="mt-2"><span className="label">{d.paper.series}</span><br /><Link href={href(locale, "/series")} className="no-underline hover:underline">{locale === "fr" ? p.series_name_fr : p.series_name_en}</Link></p>119            {p.internal_code && <p className="mt-2"><span className="label">Internal code</span><br /><span className="font-mono text-xs">{p.internal_code}</span></p>}120            <p className="mt-2"><span className="label">{d.paper.viewsLabel} / {d.paper.downloadsLabel}</span><br />{fmtInt(locale, p.view_count)} / {fmtInt(locale, p.download_count)}</p>121            <p className="mt-2 text-xs">{d.papers.popularityNote}</p>122          </div>123        </aside>124      </div>125    </article>126  );127}128
Files · main
Menu

Theme